package org.myframework.util;

import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.util.HashMap;
import java.util.Map;
import java.util.Properties;

import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.springframework.core.io.DefaultResourceLoader;
import org.springframework.core.io.Resource;
import org.springframework.core.io.ResourceLoader;
import org.springframework.util.Assert;
import org.springframework.util.DefaultPropertiesPersister;
import org.springframework.util.PropertiesPersister;

/**
 * 可定时监测Propertie文件是否修改，重新加载文件内容;
 * 代码样例：
 * 		ReloadableProperties PropertySource0 = new ReloadableProperties( );
//		PropertySource0.setFilename("classpath:application.properties");
		PropertySource0.setFilenames( new String []{"classpath:application.properties","classpath:messages.properties"});
		PropertySource0.setCacheSeconds(1);
 * <ol>
 * <li>{@link  }</li>
 *
 * </ol>
 * @see PropertiesPersister
 * @see ResourceLoader
 * @see PropertiesHolder 保存Properties配置文件的文件内容及文件时间戳，及上一次重载时间
 * @see java.util.Properties#loadFromXML(java.io.InputStream)
 * @author wanghui
 * @since 1.0
 * @2015年1月29日
 *
 */
public class ReloadableProperties extends Properties {

	/**
	 *
	 */
	private static final long serialVersionUID = -4956766104737171788L;

	protected final Log logger = LogFactory.getLog(getClass());

//	private static final String PROPERTIES_SUFFIX = ".properties";

	private static final String XML_SUFFIX = ".xml";

	private String[] filenames = new String[0];

	private String defaultEncoding;

	private Properties fileEncodings;

	private long cacheMillis = -1;

	private PropertiesPersister propertiesPersister = new DefaultPropertiesPersister();

	private ResourceLoader resourceLoader = new DefaultResourceLoader();

	/** Cache to hold filename lists per Locale */
	private final PropertiesHolder cachedMergedPropertiesHolder = new PropertiesHolder();

	/** Cache to hold already loaded properties per filename */
	private final Map<String, PropertiesHolder> cachedProperties = new HashMap<String, PropertiesHolder>();

	public void setCacheSeconds(int cacheSeconds) {
		this.cacheMillis = (cacheSeconds * 1000);
	}

	public void setFilename (String  filename ) {
		setFilenames(filename);
	}

	public void setFilenames(String... filenames) {
		if (filenames != null) {
			this.filenames = new String[filenames.length];
			for (int i = 0; i < filenames.length; i++) {
				String basename = filenames[i];
				Assert.hasText(basename, "Basename must not be empty");
				this.filenames[i] = basename.trim();
			}
		}
		else {
			this.filenames = new String[0];
		}
	}

	public void setDefaultEncoding(String defaultEncoding) {
		this.defaultEncoding = defaultEncoding;
	}

	/**
	 *
	 */
	@Override
	public  String getProperty(String code){
		if (this.cacheMillis < 0) {
			PropertiesHolder propHolder = getMergedProperties();
			String result = propHolder.getProperty(code);
			if (result != null) {
				return result;
			}
		} else {
			for (String filename : this.filenames) {
				PropertiesHolder propHolder = getProperties(filename);
				String result = propHolder.getProperty(code);
				if (result != null) {
					return result;
				}
			}
		}
		return null;

	}

	/**
	 * Get a PropertiesHolder that contains the actually visible properties for
	 * a Locale, after merging all specified resource bundles. Either fetches
	 * the holder from the cache or freshly loads it.
	 * <p>
	 * Only used when caching resource bundle contents forever, i.e. with
	 * cacheSeconds < 0. Therefore, merged properties are always cached forever.
	 */
	protected PropertiesHolder getMergedProperties() {
		Properties mergedProps = new Properties();
		for (int i = 0  ; i <this.filenames.length ; i++) {
			String filename = filenames[i];
			PropertiesHolder propHolder = getProperties(filename);
			if (propHolder.getProperties() != null) {
				mergedProps.putAll(propHolder.getProperties());
			}
		}
		cachedMergedPropertiesHolder.setRefreshTimestamp(-1);
		cachedMergedPropertiesHolder.setProperties(mergedProps);
		return cachedMergedPropertiesHolder;
	}

	/**
	 * Get a PropertiesHolder for the given filename, either from the cache or
	 * freshly loaded.
	 * 判断RefreshTimestamp和当前系统时间关系来决定是否重新加载配置文件
	 * @param filename
	 * @return the current PropertiesHolder for the bundle
	 *
	 */
	protected PropertiesHolder getProperties(String filename) {
		synchronized (this.cachedProperties) {
			PropertiesHolder propHolder = this.cachedProperties.get(filename);
			if (propHolder != null
					&& (propHolder.getRefreshTimestamp() < 0 || propHolder
							.getRefreshTimestamp() > System.currentTimeMillis()
							- this.cacheMillis)) {
				return propHolder;
			}
			return refreshProperties(filename, propHolder);
		}
	}

	/**
	 * 在文件被修改时才刷新PropertiesHolder的Properties信息。
	 * @param filename
	 *            the bundle filename
	 * @param propHolder
	 *            the current PropertiesHolder for the bundle
	 */
	protected PropertiesHolder refreshProperties(String filename,
			PropertiesHolder propHolder) {
		long refreshTimestamp = (this.cacheMillis < 0 ? -1 : System
				.currentTimeMillis());

		Resource resource = this.resourceLoader.getResource(filename );
		if (!resource.exists()) {
			resource = this.resourceLoader.getResource(filename  );
		}

		if (resource.exists()) {
			long fileTimestamp = -1;
			if (this.cacheMillis >= 0) {
				// Last-modified timestamp of file will just be read if caching
				// with timeout.
				try {
					fileTimestamp = resource.lastModified();
					if (propHolder != null
							&& propHolder.getFileTimestamp() == fileTimestamp) {
						if (logger.isDebugEnabled()) {
							logger.debug("Re-caching properties for filename ["
									+ filename
									+ "] - file hasn't been modified");
						}
						propHolder.setRefreshTimestamp(refreshTimestamp);
						return propHolder;
					}
				} catch (IOException ex) {
					// Probably a class path resource: cache it forever.
					if (logger.isDebugEnabled()) {
						logger.debug(
								resource
										+ " could not be resolved in the file system - assuming that is hasn't changed",
								ex);
					}
					fileTimestamp = -1;
				}
			}
			try {
				Properties props = loadProperties(resource, filename);
				propHolder = new PropertiesHolder(props, fileTimestamp);
			} catch (IOException ex) {
				if (logger.isWarnEnabled()) {
					logger.warn(
							"Could not parse properties file ["
									+ resource.getFilename() + "]", ex);
				}
				// Empty holder representing "not valid".
				propHolder = new PropertiesHolder();
			}
		}

		else {
			// Resource does not exist.
			if (logger.isDebugEnabled()) {
				logger.debug("No properties file found for [" + filename
						+ "] - neither plain properties nor XML");
			}
			// Empty holder representing "not found".
			propHolder = new PropertiesHolder();
		}

		propHolder.setRefreshTimestamp(refreshTimestamp);
		this.cachedProperties.put(filename, propHolder);
		return propHolder;
	}

	/**
	 * Load the properties from the given resource.
	 *
	 * @param resource
	 *            the resource to load from
	 * @param filename
	 * @return the populated Properties instance
	 * @throws IOException
	 *             if properties loading failed
	 */
	protected Properties loadProperties(Resource resource, String filename)
			throws IOException {
		InputStream is = resource.getInputStream();
		Properties props = new Properties();
		try {
			if (resource.getFilename().endsWith(XML_SUFFIX)) {
				if (logger.isDebugEnabled()) {
					logger.debug("Loading properties ["
							+ resource.getFilename() + "]");
				}
				this.propertiesPersister.loadFromXml(props, is);
			} else {
				String encoding = null;
				if (this.fileEncodings != null) {
					encoding = this.fileEncodings.getProperty(filename);
				}
				if (encoding == null) {
					encoding = this.defaultEncoding;
				}
				if (encoding != null) {
					if (logger.isDebugEnabled()) {
						logger.debug("Loading properties ["
								+ resource.getFilename() + "] with encoding '"
								+ encoding + "'");
					}
					this.propertiesPersister.load(props, new InputStreamReader(
							is, encoding));
				} else {
					if (logger.isDebugEnabled()) {
						logger.debug("Loading properties ["
								+ resource.getFilename() + "]");
					}
					this.propertiesPersister.load(props, is);
				}
			}
			return props;
		} finally {
			is.close();
		}
	}

	/**
	 * PropertiesHolder for caching. Stores the last-modified timestamp of the
	 * source file for efficient change detection, and the timestamp of the last
	 * refresh attempt (updated every time the cache entry gets re-validated).
	 */
	protected class PropertiesHolder {

		private Properties properties;

		private long fileTimestamp = -1;

		private long refreshTimestamp = -1;

		public PropertiesHolder(Properties properties, long fileTimestamp) {
			this.properties = properties;
			this.fileTimestamp = fileTimestamp;
		}

		public PropertiesHolder() {
		}

		public void setProperties(Properties properties) {
			this.properties = properties;
		}

		public Properties getProperties() {
			return properties;
		}

		public long getFileTimestamp() {
			return fileTimestamp;
		}

		public void setRefreshTimestamp(long refreshTimestamp) {
			this.refreshTimestamp = refreshTimestamp;
		}

		public long getRefreshTimestamp() {
			return refreshTimestamp;
		}

		public String getProperty(String code) {
			if (this.properties == null) {
				return null;
			}
			return this.properties.getProperty(code);
		}

	}

	public static void main(String[] args) throws IOException, InterruptedException {
		ReloadableProperties PropertySource0 = new ReloadableProperties( );
//		PropertySource0.setFilename("classpath:application.properties");
//		PropertySource0.setFilenames( new String []{"classpath:application.properties1","classpath:messages.properties"});
		PropertySource0.setCacheSeconds(1);
		String aString = PropertySource0.getProperty("support.email","");
		System.out.println(aString);
		for(int i =0 ;i<3 ;i++){
			aString = PropertySource0.getProperty("support.email");
			System.out.println(aString);
			aString = PropertySource0.getProperty("typeMismatch.user.dob");
			System.out.println(aString);
			Thread.sleep(1*1000);
		}
	}

}
